package game;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import shared.net.protocol.PacketCreator;
import shared.util.Randomizer;

/**
 * Represents a single Nim game, with 2 players, a game board, and a variable
 * number of observers.
 * @author Andrew Gratta
 * @author Vincent Tsuei
 * @version 2012-11-21
 */
public class GameInstance {
    // Game Information
    private Player p1, p2;
   
    // Observer fields
    private ArrayList<Player> observers;
    private ReentrantReadWriteLock observerLock = new ReentrantReadWriteLock();
    private GameBoard board;
    
    // Game State fields
    private GameState state;
    private Long gameID = null;
    private Boolean registered = null;
    private Player winner = null;
    private Boolean disconnect = false;
    
    /**
     * Creates a new game with two players. The board is initialized to a semi
     * random state.
     * @param p1 Player 1
     * @param p2 Player 2
     */
    public GameInstance(Player p1, Player p2)
    {
        this.p1 = p1;
        this.p2 = p2; //#FIXXXXXX
        this.state = Randomizer.getRandom().nextBoolean() ? GameState.PLAYER1_TURN : GameState.PLAYER2_TURN;
        this.observers = new ArrayList<Player>();
        this.board = new GameBoard();
    }
    
    /**
     * Add a player to the observer list
     * @param player 
     */
    public void addObserver(Player player)
    {
        observerLock.writeLock().lock();
        try {
            if (observers == null) {
                return;
            }
            observers.add(player);
        } finally {
            observerLock.writeLock().unlock();
        }
    }
    
    /**
     * Get all of the players observing this game - Threadsafe
     */
    public Player[] getObservers()
    {
        observerLock.readLock().lock();
        try {
            if (observers == null) {
                return new Player[0];
            }
            return observers.toArray(new Player[0]);
        } finally {
            observerLock.readLock().unlock();
        }
    }
    
    /**
     * Remove the specified player from the observers list
     * @param player 
     */
    public void removeObserver(Player player)
    {
        observerLock.writeLock().lock();
        try {
            if (observers == null) {
                return;
            }
            observers.remove(player);
        } finally {
            observerLock.writeLock().unlock();
        }
    }
    
    /**
     * Get the player who is currently waiting for their next turn
     * @return Player 1 is returned if it is Player 2's turn. If it is Player 1's,
     * turn, Player 2 is returned. Otherwise, null is returned.
     */
    public Player getWaitingPlayer()
    {
        if (state == GameState.PLAYER1_TURN)
            return p2;
        if (state == GameState.PLAYER2_TURN)
            return p1;
        return null;
    }
    
    /**
     * Sets the game to the Game_Over state.
     */
    public void setEndGame()
    {
        state = GameState.GAME_OVER;
    }
    
    /**
     * Returns the player who currently needs to make a move
     * @return Returns Player 1 if it is their turn, or Player 2 if it is their
     * turn. Otherwise, returns null.
     */
    public Player getActivePlayer()
    {
        if (state == GameState.PLAYER1_TURN) {
            return p1;
        } else if (state == GameState.PLAYER2_TURN) {
            return p2;
        }
        throw new RuntimeException("There is no active player.");
    }
    
    
    /**
     * 
     * @param n
     * The number of items to remove
     * @param s
     * The set to remove from
     * @param p
     * The player making the move
     * @return 
     */
    public boolean makeMove(int n, int s, Player p)
    {
        if (getActivePlayer().equals(p))
        {
            if (board.updateBoard(new PlayerMove(n, s)))
            {
                updateGameState();
                return true;
            }
        }
        return false;
    }
    
    /**
     * Advance the game state by 1 turn.
     */
    private void updateGameState()
    {
        switch (state)
        {
            case PLAYER1_TURN:
                if (board.isBoardEmpty())
                {
                    setEndGame();
                    winner = p1;
                }
                else
                {
                    state = GameState.PLAYER2_TURN;
                }
                break;
            case PLAYER2_TURN:
                if (board.isBoardEmpty())
                {
                    setEndGame();
                    winner = p2;
                }
                else
                {
                    state = GameState.PLAYER1_TURN;
                }
                break;
            default:
                throw new AssertionError(state.name());
        }
        
    }
    
    /**
     * Get the state of the game
     * @return 
     */
    public GameState getGameState()
    {
        return state;
    }
   
    /**
     * Get a string value representing the state of the game board
     * @return 
     */
    public String getBoardString()
    {
        return board.getBoardString();
    }
    
    /**
     * Return whether or not the game instance was ever registered.
     */
    public boolean wasRegistered() {
        return registered != null;
    }
    
    /**
     * Return whether or not the game instance is currently registered.
     */
    public boolean isRegistered() {
        return registered;
    }
    
    /**
     * Notify the game instance that the game has been registered.
     * Only possible if the game has NOT been registered before.
     */
    public void setRegistered(long gameID) {
        if (registered == null) {
            this.gameID = gameID;
            this.registered = true;
        } else {
            throw new RuntimeException("This GameInstance has already been registered.");
        }
    }
    
    /**
     * Notify the game instance that the game has been deregistered.
     * Only possible if the game is currently registered.
     */
    public void setDeregistered() {
        if (registered != null && registered) {
            registered = false;
            
            //Lose references to everything persistent.
            p1 = null;
            p2 = null;
            observerLock.writeLock().lock();
            try {
                observers = null;
            } finally {
                observerLock.writeLock().unlock();
            }
            winner = null;
        } else {
            throw new RuntimeException("This GameInstance has not yet been registered.");
        }
    }
    
    /**
     * Get the ID of this game.
     */
    public Long getGameID()
    {
        return gameID;
    }
    
    /**
     * Get the first player.
     */
    public Player getFirstPlayer() {
        return p1;
    }
    
    /**
     * Get the second player.
     */
    public Player getSecondPlayer() {
        return p2;
        
    }
    
    /**
     * Returns true if the game is over and there are no more moves to make
     * @return 
     */
    public boolean isOver()
    {
        return state == GameState.GAME_OVER || board.isBoardEmpty();
    }
    
    /**
     * Get the number of sets in the game.
     */
    public int getNumPiles() {
        return this.board.getNumPiles();
    }
    
    /**
     * Get the counts for the sets in the game.
     */
    public int[] getPiles() {
        return this.board.getPileCounts();
    }
    
    /**
     * Get the winner of the game. Only valid after the game is ended,
     * but before the deregistered.
     */
    public Player getWinner() {
        if (!isOver()) {
            throw new RuntimeException("Trying to get the winner of a not ended game.");
        }
        return winner;
    }
    
    /**
     * Signal that player p has quit the game - The other player wins.
     */
    public void quitGame(Player p) {
        if (p == null) {
            throw new RuntimeException("Player is null in quitGame()");
        }
        if (p == p1) {
            winner = p2;
            disconnect = true;
        } else if (p == p2) {
            winner = p1;
            disconnect = true;
        } else {
            throw new RuntimeException("Player does not belong in game.");
        }
        EndGame();
    }
    
    /**
     * End the game, then notify clients. Then unobserve observers. 
     */
    public void EndGame() {
        this.setEndGame();
        try {
            if (disconnect) {
                winner.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getDisconnectWinNotify());
            } else {
                winner.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getWinNotify());
            }
        } catch (Exception e) {
            Logger.getLogger(GameInstance.class.getName()).log(Level.INFO, "Could not notify end of game {0}", winner);
        }
        try {
            if (winner == p1) {
                if (disconnect) {
                    p2.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getDisconnectLoseNotify());
                } else {
                    p2.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getLoseNotify());
                }
            } else if (winner == p2) {
                if (disconnect) {
                    p1.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getDisconnectLoseNotify());
                } else {
                    p1.getClient().getConnection().queuePacketSend(PacketCreator.IN_GAME.getLoseNotify());
                }
            }
        } catch (Exception io) {
            Logger.getLogger(GameInstance.class.getName()).log(Level.INFO, "Could not notify end of game {0}", winner);
        }
        
        p1.leaveGame();
        p2.leaveGame();
        this.observerLock.writeLock().lock();
        try {
            for (Player o : this.observers) {
                o.unobserveGame();
            }
            this.observers.clear();
            this.observers = null;
        } finally {
            this.observerLock.writeLock().unlock();
        }
    }
    
    
}
            
